﻿#region Copyright

// Majesty of Omega - The next evolution of macro management space strategy
// 
// Copyright 2007-2009 André Claaßen
// 
// This file is protected by the terms and conditions of the
// Microsoft Community License (Ms-CL 1.1), a copy of which should
// have been distributed along with this software. If not,
// you may find the license information at the following URL:
// 
// http://www.microsoft.com/resources/sharedsource/licensingbasics/communitylicense.mspx

#endregion

#region

using System;
using System.Collections.Generic;
using System.Threading;
using DesignByContract;
using FMOD;
using Majesty_of_Omega.Config;

#endregion

namespace Majesty_of_Omega.GUI.Utility
{
    // Adapted from the original source of
    // 
    // AudioEngine.cs
    //
    // Copyright (c) 2007 Mike Strobel
    //
    // This source code is subject to the terms of the Microsoft Reciprocal License (Ms-RL).
    // For details, see <http://www.opensource.org/licenses/ms-rl.html>.
    //
    // All other rights reserved.

    namespace Supremacy.Client
    {
        /// <summary>
        /// AudioEngine, to drive the FModex.dll
        /// </summary>
        public class AudioEngine : IDisposable
        {
            /// <summary>
            /// Access to the FModex.dll API
            /// </summary>
            private readonly FMOD.System _system;

            /// <summary>
            /// Gets or sets a value indicating whether this instance is test mode. 
            /// In Testmode, you can hear no sound at all
            /// </summary>
            /// <value>
            /// 	<c>true</c> if this instance is test mode; otherwise, <c>false</c>.
            /// </value>
            public bool IsTestMode { get; set; }

            /// <summary>
            /// The Fading 
            /// </summary>
            public const float FadeStep = 0.025f;

            /// <summary>
            /// The next fading step after xx milliseconds in the updateThread()
            /// </summary>
            public int FadeStepInMilleseconds = 50;

            /// <summary>
            /// Current playing track
            /// </summary>
            private AudioEngineSoundTrack _currentTrack;

            private float _maxVolume = 1.0f;

            /// <summary>
            /// The following tracks will get faded out.
            /// </summary>
            private readonly List<AudioEngineSoundTrack> _previousTracks = new List<AudioEngineSoundTrack>();

            /// <summary>
            /// Gets the current track for testing purposes
            /// </summary>
            /// <value>The current track.</value>
            public AudioEngineSoundTrack CurrentTrack
            {
                get { return _currentTrack; }
            }

            /// <summary>
            /// Gets the previous tracks for testing purposes
            /// </summary>
            /// <value>The previous tracks.</value>
            public List<AudioEngineSoundTrack> PreviousTracks
            {
                get { return _previousTracks; }
            }

            /// <summary>
            /// Indicates, if the audio engine is running
            /// </summary>
            private bool _running;

            /// <summary>
            /// Dictionary of loaded sound files
            /// </summary>
            public Dictionary<MusicPack.TrackId, AudioEngineSoundTrack> _audioTracksDictionary =
                new Dictionary<MusicPack.TrackId, AudioEngineSoundTrack>();


            /// <summary>
            /// Gets the single instance of the Engine for this games.
            /// </summary>
            /// <remarks>
            /// Note: The Unit-Tests create their own instances
            /// </remarks>
            private static AudioEngine _instance;

            /// <summary>
            /// UpdateThread for fading in and out sounds
            /// </summary>
            private readonly Thread _updateThread;

            /// <summary>
            /// Gets the single instance of the AudioEngine
            /// </summary>
            /// <value>The instance.</value>
            public static AudioEngine Instance
            {
                get
                {
                    if (_instance == null)
                    {
                        _instance = new AudioEngine();
                    }
                    return _instance;
                }
            }

            ///<summary>
            /// Gets or sets the maximal volume
            ///</summary>
            public float MaxVolume
            {
                get { return _maxVolume; }
                set
                {
                    Require.IsTrue(value >= 0.0f && value <= 1.0f,
                                   "The max volume must lie in the range between 0.0 and 1.0");
                    _maxVolume = Math.Max(0.0f, Math.Min(value, 1.0f));
                    OnMaxVolumeChanged();
                }
            }

            /// <summary>
            /// Called when [max volume changed].
            /// </summary>
            private void OnMaxVolumeChanged()
            {
                _currentTrack.LimitToMaxVolume(MaxVolume);
                _previousTracks.ForEach(track => track.LimitToMaxVolume(MaxVolume));
            }

            ///<summary>
            /// Initialize the class
            ///</summary>
            ///<exception cref="ApplicationException">is raised, when the version of the fmodex.dll did not match</exception>
            public AudioEngine()
            {
                ErrCheck(Factory.System_Create(ref _system));
                uint version = 0;
                ErrCheck(_system.getVersion(ref version));

                if (version < VERSION.number)
                {
                    throw new ApplicationException(
                        "Error! You are using an old version of FMOD "
                        + version.ToString("X")
                        + ". This program requires "
                        + VERSION.number.ToString("X") + ".");
                }
                ErrCheck(_system.init(16, INITFLAG.NORMAL, (IntPtr) null));
                LoadMusicFromSpreadsheet();
                _updateThread = new Thread(DoUpdateFadingLogicThread);
            }

            /// <summary>
            /// Checks the result from the fmod dll. Create an exception, if there is an error
            /// </summary>
            /// <param name="result">The result.</param>
            private static void ErrCheck(RESULT result)
            {
                if (result != RESULT.OK)
                {
                    throw new ApplicationException("FMOD error! " + result + " - " + Error.String(result));
                }
            }

            /// <summary>
            /// Stops the Audio Engine and all playing sounds.
            /// </summary>
            public void Stop()
            {
                Require.IsTrue(_running, "The AudioEngine should be running, when you stop");
                _running = false;

                if (_currentTrack != null) _currentTrack.Stop();
                _previousTracks.ForEach( track => track.Stop());
                _previousTracks.Clear();

                if ((_updateThread != null) && _updateThread.IsAlive)
                    _updateThread.Interrupt();
            }

            /// <summary>
            /// Starts the audio engine
            /// </summary>
            /// <param name="startTrackId">The start track id.</param>
            public void Start(MusicPack.TrackId startTrackId)
            {
                Require.IsFalse(_running, "The AudioEngine is already started");
                _running = true;
                if (startTrackId != MusicPack.TrackId.None)
                    SwitchMusicTrack(startTrackId);
                _updateThread.Start();

            }

            /// <summary>
            /// Starts this instance with no playing title
            /// </summary>
            public void Start()
            {
                Start(MusicPack.TrackId.None);
            }

            ///<summary>
            /// Debugging Counter 
            ///</summary>
            public int NumberOfUpdates { get; set; }

            /// <summary>
            /// Does the update fading logic.
            /// </summary>
            private void DoUpdateFadingLogicThread()
            {
                while (_running)
                {
                    try
                    {
                        NumberOfUpdates++;
                        UpdateVolumeOfTracks();
                        Thread.Sleep(FadeStepInMilleseconds);
                    }
                    catch (ThreadInterruptedException)
                    {
                        return;
                    }
                }
            }

            /// <summary>
            /// Updates the playing sounds.
            /// <list type="bullet" >
            /// <item>Fading in the current track</item>
            /// <item>Fading out the previous tracks</item>
            /// </list>
            /// </summary>
            private void UpdateVolumeOfTracks()
            {
                if (_currentTrack == null) return;
                _currentTrack.FadingInTrack(FadeStep, MaxVolume);
                _previousTracks.RemoveAll(track => track.Volume == 0f);
                _previousTracks.ForEach(track => track.FadingOutTrack(FadeStep));
                _system.update();
            }

            /// <summary>
            /// Switches the music track.
            /// </summary>
            /// <param name="trackId">The track id.</param>
            public void SwitchMusicTrack(MusicPack.TrackId trackId)
            {
                Require.IsTrue(_running, "The AudioEngine is not started");
                if (_currentTrack != null &&  _currentTrack.TrackId == trackId) return;

                _previousTracks.RemoveAll(track => track.TrackId == trackId);
                if (_currentTrack != null) _previousTracks.Add(_currentTrack);
                _currentTrack = _audioTracksDictionary[trackId];
                _currentTrack.Play(_system);
            }


            /// <summary>
            /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
            /// </summary>
            public void Dispose()
            {
                if (_running) Stop();
            }

            /// <summary>
            /// Loads the music from the spreadsheet during the initialization phase
            /// </summary>
            private void LoadMusicFromSpreadsheet()
            {
                foreach (var track in SpreadSheetManager.MusicPack.MusicPackTracks)
                {
                    _audioTracksDictionary.Add(track.Id, new AudioEngineSoundTrack(_system, track));
                }
            }

            #region Nested class AudioEngineSoundTrack

            /// <summary>
            /// Encapsulate all FMOD API Calls to play a soundtrack
            /// </summary>
            public class AudioEngineSoundTrack : IDisposable
            {
                /// <summary>
                /// The Audio Channel api. The channel will set, when you play a sound
                /// </summary>
                private Channel _channel;

                /// <summary>
                /// Gets or sets the AudioChannel (API)
                /// </summary>
                /// <value>The channel.</value>
                public Channel Channel
                {
                    get { return _channel; }
                    set { _channel = value; }
                }

                /// <summary>
                /// The Sound Channel api
                /// </summary>
                private Sound _sound;

                /// <summary>
                /// Gets or sets the sound (API, Loaded OGG-File)
                /// </summary>
                /// <value>The sound.</value>
                public Sound Sound
                {
                    get { return _sound; }
                    set { _sound = value; }
                }

                /// <summary>
                /// Gets or sets the corresponding MusicPack TrackID
                /// </summary>
                /// <value>The track.</value>
                public MusicPack.TrackId TrackId { get; set; }

                /// <summary>
                /// Gets or sets the current volume of the sound track.
                /// </summary>
                /// <value>The volume.</value>
                public float Volume
                {
                    get
                    {
                        if (_channel == null) return 0;
                        float volume = 0f;
                        ErrCheck(_channel.getVolume(ref volume));
                        return volume;
                    }
                    set
                    {
                        Require.IsTrue(_channel != null, "There must be a channel open, to set the volume");
                        ErrCheck(_channel.setVolume(value));
                    }
                }

                /// <summary>
                /// Gets or sets a value indicating whether this <see cref="AudioEngineSoundTrack"/> is paused.
                /// </summary>
                /// <value><c>true</c> if paused; otherwise, <c>false</c>.</value>
                public bool Paused
                {
                    get
                    {
                        bool paused = false;
                        ErrCheck(_channel.getPaused(ref paused));
                        return paused;
                    }
                    set { ErrCheck(_channel.setPaused(value)); }
                }

                /// <summary>
                /// Initializes a new instance of the <see cref="AudioEngineSoundTrack"/> class.
                /// Loads the file and creates sound and channel
                /// </summary>
                public AudioEngineSoundTrack(FMOD.System system, MusicTrack track)
                {
                    TrackId = track.Id;
                    ErrCheck(system.createSound(
                                 Environment.CurrentDirectory + @"\Music\"+ track.Title,
                                 MODE.HARDWARE | MODE.CREATESTREAM | MODE.LOOP_NORMAL,
                                 ref _sound));
                }

                /// <summary>
                /// Plays the sound on a free channel
                /// </summary>
                /// <param name="system">The system.</param>
                public void Play(FMOD.System system)
                {
                    ErrCheck(system.playSound(
                                 CHANNELINDEX.FREE,
                                 _sound,
                                 true,
                                 ref _channel));
                    Volume = 0; // Take the fading in logic
                    Paused = false;
                }

                /// <summary>
                /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
                /// </summary>
                public void Dispose()
                {
                    ErrCheck(_sound.release());
                }

                /// <summary>
                /// Fadings the out track.
                /// </summary>
                /// <param name="FadeStep">The fade step</param>
                /// <returns>the volume was null</returns>
                public void FadingOutTrack(float FadeStep)
                {
                    Volume = Math.Max(Volume - FadeStep, 0f);
                    if (Volume == 0f )
                        Stop();
                }

                /// <summary>
                /// Fadings the in track.
                /// </summary>
                /// <param name="FadeStep">The fade step.</param>
                /// <param name="maxVolume">The max volume.</param>
                public void FadingInTrack(float FadeStep, float maxVolume)
                {
                    Volume = Math.Min(Volume + FadeStep, maxVolume);
                }

                /// <summary>
                /// Limits the current sound to max volume.
                /// </summary>
                /// <param name="maxVolume">The max volume.</param>
                public void LimitToMaxVolume(float maxVolume)
                {
                    if (Volume > maxVolume) Volume = maxVolume;
                }

                /// <summary>
                /// Stops the playing of the sound on this channel.
                /// </summary>
                public void Stop()
                {
                    ErrCheck(_channel.stop());
                    _channel = null;
                }
            }
            #endregion
        }
    }
}